Skip to content

Fix material readiness to gate on light texture readiness#18255

Merged
bghgary merged 5 commits intoBabylonJS:masterfrom
bghgary:fix/light-texture-readiness
Apr 18, 2026
Merged

Fix material readiness to gate on light texture readiness#18255
bghgary merged 5 commits intoBabylonJS:masterfrom
bghgary:fix/light-texture-readiness

Conversation

@bghgary
Copy link
Copy Markdown
Contributor

@bghgary bghgary commented Apr 8, 2026

Fix material readiness to gate on light texture readiness

[Created by Copilot on behalf of @bghgary]

Problem

SpotLight projection textures and IES profile textures were not gated by material readiness checks. When a projection texture was still loading, prepareLightSpecificDefines would set PROJECTEDLIGHTTEXTURE to false, the shader would compile without the projection effect, and the material would report isReady() = true. This caused scene.executeWhenReady() to fire before the projection texture loaded, producing incorrect rendering (e.g. intermittent visual-test failures in BabylonNative).

Root cause

  • SpotLight.prepareLightSpecificDefines() treats unloaded projection/IES textures as "not present" rather than "not ready".
  • No readiness gate existed for light texture resources in the material pipeline.
  • scene.isReady()mesh.isReady()material.isReadyForSubMesh() all returned true despite the texture still loading.

Fix

  1. Light: adds areLightTexturesReady(): boolean (default true) for subclasses that use texture resources.
  2. SpotLight: overrides areLightTexturesReady and checks _projectionTexture.isReadyOrNotBlocking() and _iesProfileTexture.isReadyOrNotBlocking() (respects BaseTexture.isBlocking semantics, matching how other material textures are gated).
  3. AreLightsTexturesReady helper (new, in materialHelper.functions.ts): iterates mesh.lightSources up to maxSimultaneousLights, respects scene.lightsEnabled and disableLighting, and mirrors the light selection used by PrepareDefinesForLights / BindLights.
  4. Materials (StandardMaterial, BackgroundMaterial, PBRBaseMaterial, OpenPBRMaterial, NodeMaterial): each calls AreLightsTexturesReady(...) in isReadyForSubMesh and returns false early if any selected light is not ready. For PBRBaseMaterial and OpenPBRMaterial the gate is placed before _prepareEffect() so we don't compile and cache a throwaway shader while textures are still loading.
  5. Node blocks (LightBlock, PBRMetallicRoughnessBlock): when an explicit this.light is bound, the block's isReady now checks this.light.areLightTexturesReady(). LightBlock registers itself as a blocking block when it has an explicit light, so the check participates in NodeMaterial._sharedData.blockingBlocks.

When a projection/IES texture finishes loading, the existing _markMeshesAsLightDirty() callback in SpotLight's projectionTexture / iesProfileTexture setters triggers re-evaluation: the helper returns true on the next readiness pass, the material reports ready, and scene.executeWhenReady() proceeds.

Tests

packages/dev/core/test/unit/Lights/babylon.spotLight.test.ts adds three regression tests:

  1. Generic contract: scene.isReady() stays false while any light reports its textures are not ready (monkey-patches light.areLightTexturesReady).
  2. SpotLight projectionTexture: scene.isReady() stays false while the projection texture is not ready.
  3. SpotLight iesProfileTexture: scene.isReady() stays false while the IES profile texture is not ready.

All three fail on master and pass on this branch. See #18336 for the test-only variant that demonstrates the master failure directly in CI.

Related

SpotLight projection textures and IES profile textures were not gated
by material readiness checks. When a projection texture was still
loading, prepareLightSpecificDefines would set PROJECTEDLIGHTTEXTURE
to false, the shader would compile without the projection effect, and
the material would report isReady()=true. This caused
scene.executeWhenReady() to fire before the projection texture loaded,
producing incorrect rendering.

Changes:
- Add areLightTexturesReady() virtual method to Light base class
- Override in SpotLight to check projection and IES textures
- Track light texture readiness in PrepareDefinesForLight state
- Propagate via defines._areLightTexturesReady in PrepareDefinesForLights
- Gate isReadyForSubMesh() on light texture readiness in all materials:
  StandardMaterial, PBRBaseMaterial, BackgroundMaterial, OpenPBRMaterial,
  NodeMaterial, and Node Material light blocks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 8, 2026 18:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a readiness gap where SpotLight projection/IES textures could still be loading while materials (and therefore scene.executeWhenReady()) reported ready, causing rendering to proceed with shaders compiled without the projected/IES lighting path.

Changes:

  • Add Light.areLightTexturesReady() (default true) and override it in SpotLight to gate on projection/IES texture readiness.
  • Propagate a lightTexturesReady flag through PrepareDefinesForLight(s) into defines._areLightTexturesReady.
  • Update core materials (Standard, PBR, OpenPBR, Background, NodeMaterial) to return not-ready when defines._areLightTexturesReady === false.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/dev/core/src/Materials/standardMaterial.ts Gates isReadyForSubMesh on defines._areLightTexturesReady.
packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts Gates PBR readiness on defines._areLightTexturesReady.
packages/dev/core/src/Materials/PBR/openpbrMaterial.ts Gates OpenPBR readiness on defines._areLightTexturesReady.
packages/dev/core/src/Materials/Node/nodeMaterial.ts Gates NodeMaterial readiness on defines._areLightTexturesReady.
packages/dev/core/src/Materials/Node/Blocks/PBR/pbrMetallicRoughnessBlock.ts Tracks per-light lightTexturesReady via PrepareDefinesForLight.
packages/dev/core/src/Materials/Node/Blocks/Dual/lightBlock.ts Tracks per-light lightTexturesReady via PrepareDefinesForLight.
packages/dev/core/src/Materials/materialHelper.ts Extends the typed PrepareDefinesForLight state contract to include lightTexturesReady.
packages/dev/core/src/Materials/materialHelper.functions.ts Implements readiness propagation (state.lightTexturesReadydefines._areLightTexturesReady) and checks light.areLightTexturesReady().
packages/dev/core/src/Materials/Background/backgroundMaterial.ts Gates BackgroundMaterial readiness on defines._areLightTexturesReady.
packages/dev/core/src/Lights/spotLight.ts Implements areLightTexturesReady() by checking projection + IES textures.
packages/dev/core/src/Lights/light.ts Adds base areLightTexturesReady() API for light subclasses.

Comment thread packages/dev/core/src/Materials/Node/Blocks/Dual/lightBlock.ts Outdated
Comment thread packages/dev/core/src/Materials/Node/Blocks/PBR/pbrMetallicRoughnessBlock.ts Outdated
Comment thread packages/dev/core/src/Lights/spotLight.ts Outdated
Comment thread packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts Outdated
Comment thread packages/dev/core/src/Materials/PBR/openpbrMaterial.ts Outdated
Comment thread packages/dev/core/src/Materials/materialHelper.functions.ts Outdated
@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 8, 2026

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s).
To prevent this PR from going to the changelog marked it with the "skip changelog" label.

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 8, 2026

Snapshot stored with reference name:
refs/pull/18255/merge

Test environment:
https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18255/merge/index.html

To test a playground add it to the URL, for example:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18255/merge/index.html#WGZLGJ#4600

Links to test your changes to core in the published versions of the Babylon tools (does not contain changes you made to the tools themselves):

https://playground.babylonjs.com/?snapshot=refs/pull/18255/merge
https://sandbox.babylonjs.com/?snapshot=refs/pull/18255/merge
https://gui.babylonjs.com/?snapshot=refs/pull/18255/merge
https://nme.babylonjs.com/?snapshot=refs/pull/18255/merge

To test the snapshot in the playground with a playground ID add it after the snapshot query string:

https://playground.babylonjs.com/?snapshot=refs/pull/18255/merge#BCU1XR#0

If you made changes to the sandbox or playground in this PR, additional comments will be generated soon containing links to the dev versions of those tools.

Copy link
Copy Markdown
Member

@sebavan sebavan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why using the defines for it ? I do not think we ever used them for it ?

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 8, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 8, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 8, 2026

@bghgary bghgary marked this pull request as draft April 9, 2026 15:31
bghgary added a commit to bghgary/Babylon.js that referenced this pull request Apr 17, 2026
Adds a unit test that demonstrates, on master, the bug that
BabylonJS#18255 fixes: scene.isReady() incorrectly returns true while a
SpotLight's projectionTexture is still loading.

The test uses NullEngine + StandardMaterial + a mocked Texture
whose isReady is pinned to false, then polls scene.isReady()
while yielding so the NullEngine shader compile can settle. On
master, scene.isReady() flips to true and the assertion fails.
On the fix branch, it stays false and the assertion passes.

This PR is expected to fail CI. It is the test-side companion
to BabylonJS#18255.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary and others added 2 commits April 17, 2026 12:38
Adds a unit test that demonstrates, on master, the bug that
BabylonJS#18255 fixes: scene.isReady() incorrectly returns true while a
SpotLight's projectionTexture is still loading.

The test uses NullEngine + StandardMaterial + a mocked Texture
whose isReady is pinned to false, then polls scene.isReady()
while yielding so the NullEngine shader compile can settle. On
master, scene.isReady() flips to true and the assertion fails.
On the fix branch, it stays false and the assertion passes.

This PR is expected to fail CI. It is the test-side companion
to BabylonJS#18255.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sProfileTexture

Extends the regression test to cover:

- The generic Light.areLightTexturesReady() contract (so future light types that add their own textures are gated automatically)

- SpotLight.iesProfileTexture readiness (the other texture SpotLight.areLightTexturesReady checks)

- SpotLight.projectionTexture readiness (unchanged)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s).
To prevent this PR from going to the changelog marked it with the "skip changelog" label.

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

Snapshot stored with reference name:
refs/pull/18255/merge

Test environment:
https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18255/merge/index.html

To test a playground add it to the URL, for example:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18255/merge/index.html#WGZLGJ#4600

Links to test your changes to core in the published versions of the Babylon tools (does not contain changes you made to the tools themselves):

https://playground.babylonjs.com/?snapshot=refs/pull/18255/merge
https://sandbox.babylonjs.com/?snapshot=refs/pull/18255/merge
https://gui.babylonjs.com/?snapshot=refs/pull/18255/merge
https://nme.babylonjs.com/?snapshot=refs/pull/18255/merge

To test the snapshot in the playground with a playground ID add it after the snapshot query string:

https://playground.babylonjs.com/?snapshot=refs/pull/18255/merge#BCU1XR#0

If you made changes to the sandbox or playground in this PR, additional comments will be generated soon containing links to the dev versions of those tools.

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

Addresses sebavan's objection to using defines for this readiness check,
and the Copilot bot comments about multi-light accumulation, blocking
semantics, and check ordering vs effect compilation.

Changes:
- Remove `defines._areLightTexturesReady` plumbing and the
  `state.lightTexturesReady` field from `PrepareDefinesForLight(s)`.
- Add `AreLightsTexturesReady(scene, mesh, maxSimultaneousLights, disableLighting)`
  helper that iterates `mesh.lightSources` up to the material's cap
  and respects `lightsEnabled` / `disableLighting`, mirroring the
  light selection used by `PrepareDefinesForLights` / `BindLights`.
- Gate readiness in each material (StandardMaterial, BackgroundMaterial,
  PBRBaseMaterial, OpenPBRMaterial, NodeMaterial) via the helper. For
  PBR/OpenPBR, the gate now runs BEFORE `_prepareEffect()` so we don't
  compile a throwaway shader while a projection/IES texture is loading.
- Node blocks with an explicit `light`: PBRMetallicRoughnessBlock's
  existing `isReady` now also checks `this.light.areLightTexturesReady()`.
  LightBlock gets an `isReady` override and registers itself as a
  blocking block when it has an explicit light.
- SpotLight.areLightTexturesReady now uses `isReadyOrNotBlocking()` to
  respect `BaseTexture.isBlocking` semantics, consistent with other
  material texture readiness checks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

@bghgary
Copy link
Copy Markdown
Contributor Author

bghgary commented Apr 17, 2026

[Responded by Copilot on behalf of @bghgary]

Why using the defines for it ? I do not think we ever used them for it ?

Good point. Refactored in 476c12a — the defines plumbing is gone. Readiness is now checked directly in each material's isReadyForSubMesh by iterating mesh.lightSources up to maxSimultaneousLights (plus explicit-light handling in LightBlock / PBRMetallicRoughnessBlock). The new helper AreLightsTexturesReady lives in materialHelper.functions.ts.

This also cleans up three other issues the Copilot bot flagged: the multi-light accumulation hazard (defines getting overwritten by the last light), PBR/OpenPBR compiling a throwaway effect before the readiness check, and SpotLight now using isReadyOrNotBlocking() to respect isBlocking semantics. Details in the individual threads.

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

@bghgary bghgary marked this pull request as ready for review April 17, 2026 21:11
@bghgary bghgary requested review from Popov72 and sebavan and removed request for sebavan April 17, 2026 21:11
@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Apr 17, 2026

@bghgary bghgary enabled auto-merge (squash) April 17, 2026 23:19
Copy link
Copy Markdown
Contributor

@Popov72 Popov72 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks good to me.

The only thing that bothers me a little—though it’s not specific to this pull request—is that we’re checking the same elements multiple times, since we’re checking them by mesh/submesh, even though the readiness status doesn’t depend on the mesh/submesh. In this pull request, it’s about lights, but there are also textures and probably other resources...

Maybe something to think about.

@bghgary bghgary merged commit 953f086 into BabylonJS:master Apr 18, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants